-
Notifications
You must be signed in to change notification settings - Fork 0
Token verification with previous public token #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Token verification with previous public token #83
Conversation
WalkthroughReplaces single-key JWT handling with a HandlerToken-based flow: config key renamed to Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant EventGate
participant LoginService
participant HandlerToken as "HandlerToken"
rect rgb(230,245,255)
Note over EventGate,LoginService: Public-keys fetch (cold start / refresh)
EventGate->>LoginService: GET /token/public-keys (TOKEN_PUBLIC_KEYS_URL)
LoginService-->>EventGate: { keys: [ { key: K1 }, { key: K0 } ] } or { key: K0 }
EventGate->>HandlerToken: load_public_keys() → parse & store RSA keys
end
Client->>EventGate: POST /event (Authorization: Bearer <token>)
EventGate->>HandlerToken: extract_token(headers)
EventGate->>HandlerToken: decode_jwt(token)
rect rgb(255,250,230)
Note over HandlerToken: Try keys sequentially (refresh if stale)
HandlerToken->>HandlerToken: verify with key[0] (may fail)
HandlerToken->>HandlerToken: verify with key[1] (may succeed)
end
alt verification succeeds
HandlerToken-->>EventGate: claims
EventGate-->>Client: 200 OK
else all keys fail
HandlerToken-->>EventGate: verification error
EventGate-->>Client: 401 Unauthorized
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
README.md(2 hunks)conf/config.json(1 hunks)src/event_gate_lambda.py(4 hunks)tests/test_conf_validation.py(1 hunks)tests/test_event_gate_lambda.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_event_gate_lambda.py (1)
src/event_gate_lambda.py (2)
lambda_handler(274-302)decode_jwt_all(225-236)
🪛 GitHub Actions: Python Check
src/event_gate_lambda.py
[error] 103-103: mypy: List comprehension has incompatible type List[DHPublicKey | DSAPublicKey | RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey | Ed448PublicKey | X25519PublicKey | X448PublicKey]; expected List[RSAPublicKey]
🪛 Ruff (0.14.4)
tests/test_event_gate_lambda.py
384-384: Unused function argument: token
(ARG001)
384-384: Unused function argument: algorithms
(ARG001)
386-386: Avoid specifying long messages outside the exception class
(TRY003)
399-399: Unused function argument: token
(ARG001)
399-399: Unused function argument: key
(ARG001)
399-399: Unused function argument: algorithms
(ARG001)
400-400: Avoid specifying long messages outside the exception class
(TRY003)
src/event_gate_lambda.py
89-89: Probable use of requests call with verify=False disabling SSL certificate checks
(S501)
100-100: Abstract raise to an inner function
(TRY301)
100-100: Avoid specifying long messages outside the exception class
(TRY003)
108-108: Avoid specifying long messages outside the exception class
(TRY003)
236-236: Avoid specifying long messages outside the exception class
(TRY003)
|
Trivy has completed a full security repository scan ✅ You can find the analysis results for this PR branch on this overview.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/event_gate_lambda.py (2)
102-104: Runtime type validation is still missing.Using
cast()satisfies the type checker but doesn't prevent non-RSA keys from enteringTOKEN_PUBLIC_KEYS, which will cause runtime failures injwt.decode. The previous review's concern remains valid: if the identity service serves an EC/DSA key during rotation, EventGate will accept it during initialization but fail unpredictably during token verification.Replace the list comprehension with an explicit loop that validates each key's type at runtime, as suggested in the previous review.
106-108: Add TypeError to exception handling.The exception handler should catch
TypeErrorto handle the runtime type validation suggested in the previous comment, ensuring consistent error reporting for all key initialization failures.Apply this diff:
-except (requests.RequestException, ValueError, KeyError, UnsupportedAlgorithm) as exc: +except (requests.RequestException, ValueError, KeyError, UnsupportedAlgorithm, TypeError) as exc: logger.exception("Failed to fetch or deserialize token public key from %s", TOKEN_PUBLIC_KEYS_URL) raise RuntimeError("Token public key initialization failed") from exc
🧹 Nitpick comments (1)
src/event_gate_lambda.py (1)
225-237: Consider adding debug logging for key rotation observability.The multi-key verification logic is correct and implements the expected behavior. However, for operational observability during key rotations, consider logging when verification fails with one key and falls back to another.
Apply this diff to add debug logging:
def decode_jwt_all(token_encoded: str) -> Dict[str, Any]: """Decode JWT using any of the loaded public keys. Args: token_encoded: Encoded bearer JWT token string. """ - for public_key in TOKEN_PUBLIC_KEYS: + for idx, public_key in enumerate(TOKEN_PUBLIC_KEYS): try: return jwt.decode(token_encoded, public_key, algorithms=["RS256"]) except jwt.PyJWTError: + logger.debug("Token verification failed with public key %d, trying next", idx) continue raise jwt.PyJWTError("Verification failed for all public keys")This would help operators confirm that key rotation is working as expected when monitoring logs.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/event_gate_lambda.py(4 hunks)
🧰 Additional context used
🪛 Ruff (0.14.4)
src/event_gate_lambda.py
89-89: Probable use of requests call with verify=False disabling SSL certificate checks
(S501)
100-100: Abstract raise to an inner function
(TRY301)
100-100: Avoid specifying long messages outside the exception class
(TRY003)
108-108: Avoid specifying long messages outside the exception class
(TRY003)
236-236: Avoid specifying long messages outside the exception class
(TRY003)
🔇 Additional comments (3)
src/event_gate_lambda.py (3)
23-23: Imports support multi-key verification.The new imports (
cast,RSAPublicKey) enable the multi-key JWT verification feature, aligning with the PR objectives.Also applies to: 31-31
84-86: Backward-compatible configuration loading.The fallback from
token_public_keys_urltotoken_public_key_urlensures compatibility during migration, which aligns with the PR's objective to support key rotation without breaking existing deployments.
183-183: Multi-key verification correctly integrated.The switch to
decode_jwt_all()enables verification against multiple public keys, fulfilling the PR's core objective of supporting key rotation without service interruption.
jakipatryk
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd add a scheduled update of the key set as well, as there is also a risk from the other direction (EventGate not yet having the new key if single Lambda instance exists for too long).
Login Service does the rotation, attaches the new key to the key set, and then for 30min doesn't yet use it for signing JWTs. So updating the key set on EventGate side with interval <30 min should be the optimal solution.
oto-macenauer-absa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good, not sure if we need to keep the legacy behavior for the keys on the other hand it's better to be on the safe side
src/event_gate_lambda.py
Outdated
| TOKEN_PROVIDER_URL = CONFIG["token_provider_url"] | ||
| # Add timeout to avoid hanging requests; wrap in robust error handling so failures are explicit | ||
| # Initialize token public keys | ||
| TOKEN_PROVIDER_URL = CONFIG.get("token_provider_url") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
put the token handling to separate module pls
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was change has a bigger impact, please see it in commit: d23819c. The project is now better readable. Need a thorough revision please.
|
Trivy has completed a full security repository scan ✅ You can find the analysis results for this PR branch on this overview.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (3)
tests/handlers/test_handler_token.py (1)
85-91: Consider adding a test for theAuthorization: Bearer <token>flow.The current tests cover the direct
Bearerheader and empty headers, but the standardAuthorizationheader with Bearer scheme (the more common pattern) isn't explicitly tested here.def test_extract_token_authorization_header(): token = HandlerToken.extract_token({"Authorization": "Bearer mytoken123"}) assert token == "mytoken123" def test_extract_token_authorization_header_case_insensitive(): token = HandlerToken.extract_token({"authorization": "bearer MYTOKEN"}) assert token == "MYTOKEN"src/handlers/handler_token.py (2)
44-47: Add validation for missingpublic_keys_url.If neither
TOKEN_PUBLIC_KEYS_URLnorTOKEN_PUBLIC_KEY_URLis present in the config,public_keys_urlwill beNone. This will causeload_public_keysto fail with an unclear error whenrequests.get(None, ...)is called.Consider validating during initialization:
def __init__(self, config): self.provider_url: str = config.get(TOKEN_PROVIDER_URL, "") self.public_keys_url: str = config.get(TOKEN_PUBLIC_KEYS_URL) or config.get(TOKEN_PUBLIC_KEY_URL) + if not self.public_keys_url: + raise ValueError("Missing required config: token_public_keys_url or token_public_key_url") self.public_keys: list[RSAPublicKey] = []
94-100: Consider differentiatingExpiredSignatureErrorfor faster failure.Currently, all
PyJWTErrorexceptions (includingExpiredSignatureError) cause the loop to try the next key. However, if a token is expired, it will be expired regardless of which key is used. You could short-circuit onExpiredSignatureErrorto avoid unnecessary iterations.for public_key in self.public_keys: try: return jwt.decode(token_encoded, public_key, algorithms=["RS256"]) + except jwt.ExpiredSignatureError: + raise # Expired tokens won't validate with any key except jwt.PyJWTError: continue raise jwt.PyJWTError("Verification failed for all public keys")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
src/event_gate_lambda.py(5 hunks)src/handlers/__init__.py(1 hunks)src/handlers/handler_token.py(1 hunks)src/utils/constants.py(1 hunks)tests/conftest.py(1 hunks)tests/handlers/__init__.py(1 hunks)tests/handlers/test_handler_token.py(1 hunks)tests/test_event_gate_lambda.py(1 hunks)tests/test_event_gate_lambda_local_access.py(1 hunks)tests/utils/test_extract_token.py(0 hunks)
💤 Files with no reviewable changes (1)
- tests/utils/test_extract_token.py
✅ Files skipped from review due to trivial changes (2)
- src/handlers/init.py
- tests/handlers/init.py
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/test_event_gate_lambda.py
🧰 Additional context used
🧬 Code graph analysis (3)
tests/handlers/test_handler_token.py (2)
src/handlers/handler_token.py (3)
HandlerToken(39-142)decode_jwt(84-100)extract_token(110-142)src/event_gate_lambda.py (1)
lambda_handler(192-220)
tests/conftest.py (2)
src/writers/writer_kafka.py (1)
KafkaException(35-36)tests/test_event_gate_lambda_local_access.py (1)
Bucket(46-47)
src/event_gate_lambda.py (1)
src/handlers/handler_token.py (5)
HandlerToken(39-142)load_public_keys(49-82)decode_jwt(84-100)get_token(102-107)extract_token(110-142)
🪛 Ruff (0.14.5)
tests/handlers/test_handler_token.py
61-61: Unused function argument: token
(ARG001)
61-61: Unused function argument: algorithms
(ARG001)
61-61: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
63-63: Avoid specifying long messages outside the exception class
(TRY003)
76-76: Unused function argument: token
(ARG001)
76-76: Unused function argument: key
(ARG001)
76-76: Unused function argument: algorithms
(ARG001)
76-76: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
77-77: Avoid specifying long messages outside the exception class
(TRY003)
91-91: Possible hardcoded password assigned to: "token"
(S105)
src/handlers/handler_token.py
60-60: Probable use of requests call with verify=False disabling SSL certificate checks
(S501)
72-72: Abstract raise to an inner function
(TRY301)
72-72: Avoid specifying long messages outside the exception class
(TRY003)
79-79: Consider moving this statement to an else block
(TRY300)
82-82: Avoid specifying long messages outside the exception class
(TRY003)
100-100: Avoid specifying long messages outside the exception class
(TRY003)
tests/conftest.py
61-61: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
97-97: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
101-101: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
src/utils/constants.py
22-22: Possible hardcoded password assigned to: "TOKEN_PROVIDER_URL"
(S105)
23-23: Possible hardcoded password assigned to: "TOKEN_PUBLIC_KEY_URL"
(S105)
24-24: Possible hardcoded password assigned to: "TOKEN_PUBLIC_KEYS_URL"
(S105)
🔇 Additional comments (14)
src/utils/constants.py (1)
21-24: LGTM!The constants are well-defined config key names. The static analysis warnings (S105) are false positives—these are dictionary keys for configuration lookup, not hardcoded secrets.
tests/test_event_gate_lambda_local_access.py (1)
57-57: LGTM on the attribute rename.The change from
CONFIGtoconfigaligns with the module refactoring.However, note that
local_configat line 15 still uses"token_public_key_url"instead of the new"token_public_keys_url". This works due to the fallback inHandlerToken.__init__, but consider updating it for consistency with the renamed config key.tests/handlers/test_handler_token.py (2)
25-51: Well-structured tests for core token handling.The tests cover the key scenarios: redirect, expired token, and multi-key verification. The mocking approach using
event_gate_modulefixture is clean.
54-82: Good coverage of multi-key JWT verification logic.These tests validate the core feature: falling back to the second key when the first fails, and raising an aggregate error when all keys fail.
tests/conftest.py (2)
33-121: Well-designed centralized fixture for module testing.The
event_gate_modulefixture effectively isolates external dependencies and provides a clean testing environment. The approach of conditionally creating dummy modules only when the real ones are unavailable is pragmatic.One minor observation: the fixture uses both
started_patcheslist andExitStackfor cleanup. This works correctly, but theExitStackcould handle all cleanup uniformly if the patches were entered viaexit_stack.enter_context(p)instead ofp.start().
124-149: Clean helper fixtures.
make_eventandvalid_payloadprovide good reusable test utilities.src/handlers/handler_token.py (1)
109-142: Well-implemented token extraction with robust header handling.The case-insensitive header normalization and support for both direct
Bearerheader and standardAuthorization: Bearerscheme is well thought out.src/event_gate_lambda.py (7)
30-30: LGTM!Clean import of the new
HandlerTokenmodule, which consolidates token-related functionality as requested in past reviews.
64-64: Consistent naming convention applied.The rename from
CONFIGtoconfigaligns with Python naming conventions (lowercase for module-level variables that aren't true constants). Ensure any external references to this module's config are updated if applicable.
70-77: LGTM!Config access updated consistently to use the renamed
configvariable.
80-86: Clean initialization with appropriate fail-fast behavior.The chained
HandlerToken(config).load_public_keys()is clean and ensures the Lambda fails at cold start if public keys cannot be loaded—appropriate for a security-critical path. The handler'sload_public_keys()already logs and raises a descriptiveRuntimeErroron failure.Note: Keys are loaded once at module initialization. If keys rotate while the Lambda instance is warm, it won't pick up new keys until the instance is recycled. This is typically acceptable for Lambda's lifecycle, but worth documenting if not already covered.
150-152: Core change for key rotation support looks correct.The
decode_jwt()method inHandlerTokeniterates through all loaded public keys, enabling seamless verification during key rotation. The exception handling correctly catchesjwt.PyJWTErrorwhen all keys fail verification.
202-202: LGTM!Delegation to
handler_token.get_token()is clean and maintains the same 303 redirect behavior.
213-213: LGTM!Delegation to the static
extract_token()method is appropriate. The method handles both theBearerheader andAuthorization: Bearer <token>formats with proper case-insensitive lookup.
| try: | ||
| response_json = requests.get(self.public_keys_url, verify=False, timeout=5).json() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security concern: SSL certificate verification is disabled.
Using verify=False disables SSL certificate verification, making this request vulnerable to man-in-the-middle attacks. This is particularly concerning when fetching cryptographic public keys, as an attacker could inject malicious keys.
Consider making SSL verification configurable or defaulting to verify=True:
- response_json = requests.get(self.public_keys_url, verify=False, timeout=5).json()
+ response_json = requests.get(self.public_keys_url, timeout=5).json()If there's a legitimate need to disable verification in certain environments (e.g., development with self-signed certs), consider making it configurable via an environment variable or config parameter rather than unconditionally disabling it.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try: | |
| response_json = requests.get(self.public_keys_url, verify=False, timeout=5).json() | |
| try: | |
| response_json = requests.get(self.public_keys_url, timeout=5).json() |
🧰 Tools
🪛 Ruff (0.14.5)
60-60: Probable use of requests call with verify=False disabling SSL certificate checks
(S501)
🤖 Prompt for AI Agents
In src/handlers/handler_token.py around lines 59-60, the requests.get call
disables SSL certificate verification (verify=False) which is insecure; change
it to use a configurable verification flag (e.g., read from an environment
variable or config with default True) and pass that flag to requests.get
(verify=<config_flag>) instead of False, ensure the default is True, add a log
warning if verification is explicitly disabled, and do not leave verify=False
hard-coded in the repository.
|
@jakipatryk Thank you for the comment! I did create an issue describing your comment, which is now in our ToDo section with a high priority #87. I will send you the solution for revision, when it is ready to. |
oto-macenauer-absa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
* Periodic JWT Public Key Refresh Implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/handlers/handler_token.py (1)
81-82: Security: SSL verification disabled (already flagged).This concern was raised in a previous review. Using
verify=Falsewhen fetching cryptographic keys creates MITM vulnerability.
🧹 Nitpick comments (4)
tests/handlers/test_handler_token.py (2)
70-73: Remove unusednoqadirectives.The
noqa: D401directives on lines 70 and 85 are unused since this rule is not enabled. The unused parameters (token,algorithms,key) in mock stubs are intentional to match thejwt.decodesignature.- def decode_side_effect(token, key, algorithms): # noqa: D401 - test stub + def decode_side_effect(token, key, algorithms):- def always_fail(token, key, algorithms): # noqa: D401 - test stub + def always_fail(token, key, algorithms):
94-100: Consider adding test for Authorization header extraction.The current tests cover empty headers and direct Bearer header. Consider adding coverage for the
Authorization: Bearer <token>format which is also supported byextract_token.def test_extract_token_authorization_header(): token = HandlerToken.extract_token({"Authorization": "Bearer mytoken123"}) assert token == "mytoken123"src/handlers/handler_token.py (2)
47-51: Consider validatingpublic_keys_urlis not None.If neither
TOKEN_PUBLIC_KEYS_URLnorTOKEN_PUBLIC_KEY_URLare present in config,public_keys_urlwill beNone, causingrequests.get(None, ...)to fail with a confusing error inload_public_keys.def __init__(self, config): self.provider_url: str = config.get(TOKEN_PROVIDER_URL, "") - self.public_keys_url: str = config.get(TOKEN_PUBLIC_KEYS_URL) or config.get(TOKEN_PUBLIC_KEY_URL) + self.public_keys_url: str = config.get(TOKEN_PUBLIC_KEYS_URL) or config.get(TOKEN_PUBLIC_KEY_URL) or "" self.public_keys: list[RSAPublicKey] = [] self._last_loaded_at: datetime | None = NoneAlternatively, raise a
ValueErrorin__init__ifpublic_keys_urlis falsy to fail fast with a clear error message.
96-98: Unsafe cast toRSAPublicKeymay fail at runtime.
serialization.load_der_public_key()can return different key types (RSA, EC, Ed25519, etc.). If the key is not RSA, the cast will succeed but subsequent usage withjwt.decode(expecting RSA for RS256) may fail with an unclear error.- self.public_keys = [ - cast(RSAPublicKey, serialization.load_der_public_key(base64.b64decode(raw_key))) for raw_key in raw_keys - ] + loaded_keys = [] + for raw_key in raw_keys: + key = serialization.load_der_public_key(base64.b64decode(raw_key)) + if not isinstance(key, RSAPublicKey): + raise ValueError(f"Expected RSA public key, got {type(key).__name__}") + loaded_keys.append(key) + self.public_keys = loaded_keys
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/handlers/handler_token.py(1 hunks)tests/handlers/test_handler_token.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/handlers/test_handler_token.py (1)
src/handlers/handler_token.py (4)
HandlerToken(40-167)decode_jwt(107-125)extract_token(135-167)_refresh_keys_if_needed(53-69)
🪛 Ruff (0.14.6)
tests/handlers/test_handler_token.py
70-70: Unused function argument: token
(ARG001)
70-70: Unused function argument: algorithms
(ARG001)
70-70: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
72-72: Avoid specifying long messages outside the exception class
(TRY003)
85-85: Unused function argument: token
(ARG001)
85-85: Unused function argument: key
(ARG001)
85-85: Unused function argument: algorithms
(ARG001)
85-85: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
86-86: Avoid specifying long messages outside the exception class
(TRY003)
100-100: Possible hardcoded password assigned to: "token"
(S105)
src/handlers/handler_token.py
82-82: Probable use of requests call with verify=False disabling SSL certificate checks
(S501)
94-94: Abstract raise to an inner function
(TRY301)
94-94: Avoid specifying long messages outside the exception class
(TRY003)
102-102: Consider moving this statement to an else block
(TRY300)
105-105: Avoid specifying long messages outside the exception class
(TRY003)
125-125: Avoid specifying long messages outside the exception class
(TRY003)
🔇 Additional comments (9)
tests/handlers/test_handler_token.py (5)
17-31: LGTM!Imports and fixture are well-structured. The fixture provides a clean test instance with the required config.
34-38: LGTM!Clean integration test validating the 303 redirect behavior for the token endpoint.
41-60: LGTM!Good test coverage for expired JWT handling with proper mocking and assertions.
124-132: LGTM!Good resilience test ensuring existing keys are preserved when refresh fails.
135-143: LGTM!Properly validates that JWT decoding triggers the key freshness check.
src/handlers/handler_token.py (4)
17-38: LGTM!Module docstring, imports, and logger configuration are appropriate and well-organized.
53-69: LGTM!The refresh logic correctly handles the initial unloaded state and gracefully degrades when refresh fails by keeping existing keys.
127-132: LGTM!The redirect response structure is correct. The naming suggestion from a previous review (e.g.,
get_token_provider_info) could improve clarity but is optional.
134-167: LGTM!Robust token extraction with proper case-insensitive handling and support for both Bearer header and Authorization schemes.
| for public_key in self.public_keys: | ||
| try: | ||
| return jwt.decode(token_encoded, public_key, algorithms=["RS256"]) | ||
| except jwt.PyJWTError: | ||
| continue | ||
| raise jwt.PyJWTError("Verification failed for all public keys") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overly broad exception handling catches non-signature errors.
Catching all PyJWTError means expired tokens (ExpiredSignatureError) and malformed tokens (DecodeError) will unnecessarily try every key before failing. Only signature-related errors should trigger trying the next key.
for public_key in self.public_keys:
try:
return jwt.decode(token_encoded, public_key, algorithms=["RS256"])
- except jwt.PyJWTError:
+ except jwt.InvalidSignatureError:
continue
+ except jwt.PyJWTError:
+ raise # Expired, malformed, etc. - fail immediately
raise jwt.PyJWTError("Verification failed for all public keys")This ensures expired or malformed tokens fail fast with the correct error, while signature mismatches correctly try the next key in the rotation.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for public_key in self.public_keys: | |
| try: | |
| return jwt.decode(token_encoded, public_key, algorithms=["RS256"]) | |
| except jwt.PyJWTError: | |
| continue | |
| raise jwt.PyJWTError("Verification failed for all public keys") | |
| for public_key in self.public_keys: | |
| try: | |
| return jwt.decode(token_encoded, public_key, algorithms=["RS256"]) | |
| except jwt.InvalidSignatureError: | |
| continue | |
| except jwt.PyJWTError: | |
| raise # Expired, malformed, etc. - fail immediately | |
| raise jwt.PyJWTError("Verification failed for all public keys") |
🧰 Tools
🪛 Ruff (0.14.6)
125-125: Avoid specifying long messages outside the exception class
(TRY003)
🤖 Prompt for AI Agents
In src/handlers/handler_token.py around lines 120-125, the loop currently
catches all jwt.PyJWTError which masks expired or malformed token errors; change
the logic to only catch signature-related exceptions (e.g.,
jwt.InvalidSignatureError or jwt.exceptions.InvalidSignatureError) inside the
loop so only signature mismatches try the next public key, and let other
PyJWTError subclasses (ExpiredSignatureError, DecodeError, etc.) propagate
immediately; implement this by catching InvalidSignatureError in the except
branch (or store the last signature error and continue) and re-raising any other
PyJWTError after the loop (or re-raise immediately when caught).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (3)
tests/handlers/test_handler_token.py (1)
104-121: Docstrings correctly reference 28 minutes.The docstrings now accurately reflect the
_REFRESH_INTERVALof 28 minutes, addressing the previous review feedback.src/handlers/handler_token.py (2)
79-83: Do not hard-codeverify=Falsewhen fetching public keys.
requests.get(..., verify=False, ...)disables TLS certificate verification and opens a serious MITM risk, especially dangerous when downloading public keys used for JWT verification. This concern was already raised previously and still applies.Consider making TLS verification configurable but default-on, and logging when it is disabled, e.g.:
@@ -logger = logging.getLogger(__name__) -log_level = os.environ.get("LOG_LEVEL", "INFO") -logger.setLevel(log_level) +logger = logging.getLogger(__name__) +log_level = os.environ.get("LOG_LEVEL", "INFO") +logger.setLevel(log_level) + +# Controls SSL certificate verification when fetching token public keys. +# Default: verify certificates. Set TOKEN_PUBLIC_KEYS_VERIFY_TLS=false to disable (e.g. in dev). +_verify_env = os.environ.get("TOKEN_PUBLIC_KEYS_VERIFY_TLS", "true").lower() +TOKEN_PUBLIC_KEYS_VERIFY_TLS = _verify_env not in {"0", "false", "no"} @@ - try: - response_json = requests.get(self.public_keys_url, verify=False, timeout=5).json() + try: + if not TOKEN_PUBLIC_KEYS_VERIFY_TLS: + logger.warning( + "SSL certificate verification is disabled for token public keys URL %s", + self.public_keys_url, + ) + response_json = requests.get( + self.public_keys_url, + verify=TOKEN_PUBLIC_KEYS_VERIFY_TLS, + timeout=5, + ).json()If your config system already has a place for this flag, you can wire it through there instead of using an env var, but the key points are: default to
verify=True, make disabling explicit, and never leaveverify=Falsehard-coded.
119-125: Narrow JWT exception handling to signature mismatches only.Catching all
jwt.PyJWTErrormeans expired tokens, malformed tokens, etc. will unnecessarily try every key and then surface as a generic"Verification failed for all public keys", masking the real cause. This was called out in a prior review and is still present.A pattern that keeps retries only for signature mismatches:
- logger.debug("Decoding JWT") - for public_key in self.public_keys: - try: - return jwt.decode(token_encoded, public_key, algorithms=["RS256"]) - except jwt.PyJWTError: - continue - raise jwt.PyJWTError("Verification failed for all public keys") + logger.debug("Decoding JWT") + last_signature_error: jwt.PyJWTError | None = None + + for public_key in self.public_keys: + try: + return jwt.decode(token_encoded, public_key, algorithms=["RS256"]) + except jwt.PyJWTError as exc: + # Only retry on signature mismatch; propagate other JWT errors immediately. + if isinstance(exc, jwt.exceptions.InvalidSignatureError): + last_signature_error = exc + continue + raise + + if last_signature_error is not None: + # All keys tried, but signatures didn't match. + raise last_signature_error + raise jwt.PyJWTError("Verification failed: no public keys succeeded")Please verify against the PyJWT version you use that
jwt.exceptions.InvalidSignatureErroris the correct class to distinguish signature mismatches.
🧹 Nitpick comments (6)
tests/conftest.py (2)
61-61: Remove unusednoqadirective.Per static analysis, the
D401rule is not enabled, making the# noqa: D401comment unnecessary.- def flush(self): # noqa: D401 - simple stub + def flush(self):
97-101: Remove unusednoqadirectives.The
# noqa: D401comments on lines 97 and 101 are unnecessary sinceD401is not enabled.- def Object(self, _key): # noqa: D401 - simple proxy + def Object(self, _key): return MockS3Object() class MockS3Resource: - def Bucket(self, _name): # noqa: D401 - simple proxy + def Bucket(self, _name): return MockS3Bucket()tests/handlers/test_handler_token.py (2)
70-70: Remove unusednoqadirective.The
# noqa: D401comment is unnecessary sinceD401is not enabled.- def decode_side_effect(token, key, algorithms): # noqa: D401 - test stub + def decode_side_effect(token, key, algorithms):
85-85: Remove unusednoqadirective.- def always_fail(token, key, algorithms): # noqa: D401 - test stub + def always_fail(token, key, algorithms):src/handlers/handler_token.py (2)
47-52: Fail fast on missing public keys URL in config.If both
TOKEN_PUBLIC_KEYS_URL_KEYandTOKEN_PUBLIC_KEY_URL_KEYare absent,self.public_keys_urlbecomesNone, andload_public_keyswill later fail viarequests.get(None, ...)with a less explicit error.Consider validating this in
__init__and raising a clear configuration error early, e.g.:def __init__(self, config): self.provider_url: str = config.get(TOKEN_PROVIDER_URL_KEY, "") - self.public_keys_url: str = config.get(TOKEN_PUBLIC_KEYS_URL_KEY) or config.get(TOKEN_PUBLIC_KEY_URL_KEY) + self.public_keys_url: str | None = ( + config.get(TOKEN_PUBLIC_KEYS_URL_KEY) or config.get(TOKEN_PUBLIC_KEY_URL_KEY) + ) self.public_keys: list[RSAPublicKey] = [] self._last_loaded_at: datetime | None = None + + if not self.public_keys_url: + raise ValueError("Token public keys URL must be configured")This makes misconfiguration easier to diagnose and avoids opaque networking errors.
53-70: Clarify behavior when no public keys have ever been loaded.If
_last_loaded_atisNone,_refresh_keys_if_neededreturns without loading, anddecode_jwtthen iterates over an emptyself.public_keysand raises a genericPyJWTError. That makes first-use behavior dependent on external callers remembering to callload_public_keys().Either lazily load on first use or fail with a more explicit error. For example:
def _refresh_keys_if_needed(self) -> None: @@ - if self._last_loaded_at is None: - return + if self._last_loaded_at is None: + logger.debug("Token public keys not loaded yet, loading now") + try: + self.load_public_keys() + except RuntimeError: + logger.warning("Initial token public key load failed") + returnand/or in
decode_jwtguard against an empty key set:def decode_jwt(self, token_encoded: str) -> Dict[str, Any]: self._refresh_keys_if_needed() logger.debug("Decoding JWT") + if not self.public_keys: + raise jwt.PyJWTError("No token public keys loaded for verification")This makes operational issues much easier to debug.
Also applies to: 117-125
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/event_gate_lambda.py(5 hunks)src/handlers/handler_token.py(1 hunks)src/utils/constants.py(1 hunks)tests/conftest.py(1 hunks)tests/handlers/test_handler_token.py(1 hunks)tests/test_event_gate_lambda_local_access.py(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/conftest.py (2)
src/writers/writer_kafka.py (1)
KafkaException(35-36)tests/test_event_gate_lambda_local_access.py (1)
Bucket(47-48)
src/event_gate_lambda.py (2)
src/handlers/handler_token.py (5)
HandlerToken(40-167)load_public_keys(71-105)decode_jwt(107-125)get_token_provider_info(127-132)extract_token(135-167)src/writers/writer_eventbridge.py (1)
init(34-44)
🪛 Ruff (0.14.6)
src/handlers/handler_token.py
82-82: Probable use of requests call with verify=False disabling SSL certificate checks
(S501)
94-94: Abstract raise to an inner function
(TRY301)
94-94: Avoid specifying long messages outside the exception class
(TRY003)
102-102: Consider moving this statement to an else block
(TRY300)
105-105: Avoid specifying long messages outside the exception class
(TRY003)
125-125: Avoid specifying long messages outside the exception class
(TRY003)
tests/conftest.py
61-61: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
97-97: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
101-101: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
tests/handlers/test_handler_token.py
70-70: Unused function argument: token
(ARG001)
70-70: Unused function argument: algorithms
(ARG001)
70-70: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
72-72: Avoid specifying long messages outside the exception class
(TRY003)
85-85: Unused function argument: token
(ARG001)
85-85: Unused function argument: key
(ARG001)
85-85: Unused function argument: algorithms
(ARG001)
85-85: Unused noqa directive (non-enabled: D401)
Remove unused noqa directive
(RUF100)
86-86: Avoid specifying long messages outside the exception class
(TRY003)
100-100: Possible hardcoded password assigned to: "token"
(S105)
src/utils/constants.py
22-22: Possible hardcoded password assigned to: "TOKEN_PROVIDER_URL_KEY"
(S105)
23-23: Possible hardcoded password assigned to: "TOKEN_PUBLIC_KEY_URL_KEY"
(S105)
24-24: Possible hardcoded password assigned to: "TOKEN_PUBLIC_KEYS_URL_KEY"
(S105)
🔇 Additional comments (14)
src/utils/constants.py (1)
21-24: LGTM - Config key constants are well-defined.The
_KEYsuffix clearly indicates these are configuration key names rather than actual URLs. The Ruff S105 warnings about "hardcoded passwords" are false positives—these variables contain config key strings, not sensitive data.tests/test_event_gate_lambda_local_access.py (3)
39-44: LGTM - Kafka producer mock properly configured.The mock setup correctly captures the producer patch and sets a
MagicMock()return value, preventing actual Kafka connections during test execution.
58-59: LGTM - Config access updated to lowercaseconfig.The change from
CONFIGtoconfigaligns with the module-level variable rename insrc/event_gate_lambda.py.
12-18: No changes needed; the test correctly implements backward compatibility testing.The
local_configusestoken_public_key_url(singular) as intended. TheHandlerTokenclass implements a fallback chain that first checks for the newtoken_public_keys_url(plural) key, then falls back totoken_public_key_url(singular), ensuring compatibility with legacy configurations. The test validates this fallback behavior by providing only the legacy key.tests/conftest.py (2)
33-121: Well-structured centralized test fixture.The
event_gate_modulefixture effectively centralizes heavy environment setup, reducing duplication across test modules. The use ofExitStackfor managingsys.modulespatches and explicitp.stop()for started patches ensures proper cleanup.
124-149: LGTM - Helper fixtures are clean and reusable.
make_eventandvalid_payloadfixtures provide clear, minimal abstractions for constructing test data.tests/handlers/test_handler_token.py (3)
27-31: LGTM - Token handler fixture is properly configured.The fixture creates a
HandlerTokeninstance with a sample config for isolated unit testing.
63-77: Good coverage of multi-key verification scenario.This test validates the core PR objective—verifying tokens against multiple keys when the first key fails. The test correctly simulates the fallback behavior during key rotation.
124-132: Good error resilience test.This test ensures graceful degradation when key refresh fails—existing keys are retained and a warning is logged, maintaining service availability during transient network issues.
src/event_gate_lambda.py (4)
30-31: Clean import addition for HandlerToken.The new import properly integrates the refactored token handling module.
80-86: Well-structured initialization sequence.The initialization order is correct: config is loaded first, then
HandlerTokenis instantiated and keys are loaded via method chaining, followed by writer initialization. This ensures all dependencies are available when needed.
149-152: Token decoding properly delegated to HandlerToken.The multi-key verification logic is now encapsulated in
handler_token.decode_jwt(), which will try both current and previous public keys during key rotation, addressing the PR's core objective.
201-213: Token-related endpoints properly delegated.Both
/tokenredirect and token extraction are now handled viaHandlerToken, improving separation of concerns and maintainability. This refactoring consolidates all token-related logic in one place.src/handlers/handler_token.py (1)
135-167: Token extraction helper looks robust and covers common header patterns.The normalization to lowercase keys, support for both a direct
Bearerheader andAuthorization: Bearer ..., and defensive type checks make this helper solid and resilient to slightly messy input. No issues from my side here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
tests/conftest.py(1 hunks)tests/handlers/test_handler_token.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/conftest.py (2)
src/writers/writer_kafka.py (1)
KafkaException(35-36)tests/test_event_gate_lambda_local_access.py (1)
Bucket(47-48)
tests/handlers/test_handler_token.py (1)
src/handlers/handler_token.py (3)
decode_jwt(107-125)extract_token(135-167)_refresh_keys_if_needed(53-69)
🪛 Ruff (0.14.6)
tests/handlers/test_handler_token.py
70-70: Unused function argument: token
(ARG001)
70-70: Unused function argument: algorithms
(ARG001)
72-72: Avoid specifying long messages outside the exception class
(TRY003)
85-85: Unused function argument: token
(ARG001)
85-85: Unused function argument: key
(ARG001)
85-85: Unused function argument: algorithms
(ARG001)
86-86: Avoid specifying long messages outside the exception class
(TRY003)
100-100: Possible hardcoded password assigned to: "token"
(S105)
🔇 Additional comments (14)
tests/conftest.py (3)
33-122: LGTM!The fixture properly centralizes heavy environment setup with comprehensive mocking of external dependencies (Kafka, PostgreSQL, S3, Events client). The use of
ExitStackfor cleanup and conditional dummy module creation is well-implemented.
124-138: LGTM!The factory fixture provides a clean interface for building test events with sensible defaults.
140-149: LGTM!Provides a canonical test payload with all required fields.
tests/handlers/test_handler_token.py (11)
27-31: LGTM!The fixture properly initializes
HandlerTokenwith the expected configuration structure.
34-38: LGTM!Correctly validates the GET /token redirect behavior.
41-61: LGTM!Properly tests expired token handling with appropriate mocking and assertions.
63-78: LGTM!Correctly validates multi-key JWT verification fallback behavior. The unused parameters in the
decode_side_effectfunction (line 70) flagged by static analysis are acceptable—the signature must matchjwt.decode.
80-91: LGTM!Correctly validates the error case when all keys fail verification. Unused parameters in the
always_failfunction (line 85) are acceptable for test mocks.
94-95: LGTM!Correctly validates empty headers case.
98-100: LGTM!Correctly validates direct bearer header extraction with whitespace trimming. The static analysis warning about a hardcoded password is a false positive for test data.
104-111: LGTM!Correctly validates that fresh keys don't trigger refresh. The docstring accurately reflects the 28-minute refresh interval.
114-121: LGTM!Correctly validates that stale keys trigger refresh. The docstring accurately reflects the 28-minute refresh interval.
124-132: LGTM!Correctly validates graceful degradation when key refresh fails.
135-144: LGTM!Correctly validates that JWT decoding triggers a refresh check before processing.
|
Trivy has completed a full security repository scan ✅ You can find the analysis results for this PR branch on this overview.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
tests/conftest.py (1)
33-122: Consider test isolation implications of module-scoped fixture.The
event_gate_modulefixture usesscope="module", meaning it's shared across all tests in a test module. While this improves performance by reducing setup overhead, it can lead to test coupling if tests modify the imported module's state or its mocked dependencies. Ensure that tests using this fixture are stateless or properly reset any modified state.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
tests/conftest.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/conftest.py (2)
src/writers/writer_kafka.py (1)
KafkaException(35-36)tests/test_event_gate_lambda_local_access.py (1)
Bucket(47-48)
🔇 Additional comments (3)
tests/conftest.py (3)
16-24: Previous import issue resolved.The missing
importlib.utilimport flagged in the previous review has been correctly added on line 18.
124-149: LGTM!The
make_eventandvalid_payloadfixtures are well-designed helpers that provide clean test data construction. The factory pattern inmake_eventoffers good flexibility for building different event types.
76-77: Verify that the mocked response format aligns with the token handler's expectations.The mock in conftest.py line 77 uses the single-key format
{"key": "..."}. Ensure that:
- If the new token handler expects a multi-key format
{"keys": [...]}, update this mock to match- If single-key format is correct (e.g., for backward compatibility testing), verify that tests explicitly document this intent
- Tests using this fixture should validate the code path being tested (new multi-key logic vs. fallback handling)
jakipatryk
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM (just scanned through the code).
Overview
Adding a logic for verification to solve a new pair of public token to fetch current as well as previous one.
Release Notes:
Related
Closes #65
Summary by CodeRabbit
New Features
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.